<?php
/**
 * 
 * Tagging model
 * 
 * @category Abovo
 * 
 * @package Abovo_Model_Tag
 * 
 * @author Paul M. Jones <pmjones@solarphp.com>
 * 
 * @author Antti Holvikari <anttih@gmail.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id$
 * 
 */

/**
 * 
 * Tagging model
 * 
 * Based on the 'toxi' tagging table schema described
 * [here](http://www.pui.ch/phred/archives/2005/04/tags-database-schemas.html).
 * 
 * @category Abovo
 * 
 * @package Abovo_Model_Tag
 * 
 */
class Abovo_Model_Tag extends Solar_Base {
    
    /**
     * Area name
     * 
     * Area to which all the tags belong to
     *
     * @var string
     */
    protected $_area;
    
    /**
     * Tags table model
     *
     * @var Abovo_Model_Tags
     */
    private $_tags;
    
    /**
     * Tagmap table model
     *
     * @var Abovo_Model_TagMap
     */
    private $_tagmap;
    
    /**
     * 
     * Constructor
     * 
     * @return void
     * 
     */
    public function __construct($config = array())
    {
        parent::__construct($config);
        
        if (empty($this->_config['area'])) {
            throw $this->_exception('ERR_AREA_NOT_SET');
        } else {
            $this->_area = $this->_config['area'];
        }
        
        // Make sure sql is available
        if (! Solar::isRegistered('sql')) {
            Solar::register('sql', Solar::factory('Solar_Sql'));
        }
        
        // Instantiate table models, this will create the tables
        $this->_tags   = Solar::factory('Abovo_Model_Tags');
        $this->_tagmap = Solar::factory('Abovo_Model_TagMap');
    }
    
    /**
     * 
     * Fetches all tags
     * 
     * @param int $node_id Node id for which to search tags for
     * 
     * @param string $handle User handle for which to search tags for
     * 
     * @return array List of tags
     * 
     */
    public function fetchAll($node_id = null, $handle = null)
    {
        $select = Solar::factory('Solar_Sql_Select');
        
        // table names
        $tagmap = $this->_tagmap->name;
        $tags   = $this->_tags->name;
        
        $select->from($tags, 'name')
               ->leftJoin($tagmap, "$tags.id = $tagmap.tags_id")
               ->where($tagmap . '.area = ?', $this->_area);
        
        // fetch for node_id?
        if (is_numeric($node_id)) {
            $select->where($tagmap . '.node_id = ?', (int) $node_id);
        }
        
        // fetch for handle?
        if (! empty($handle)) {
            $select->where("$tagmap.handle = ?", $handle);
        }
        
        $select->group("$tags.name");
        return $select->fetch('col');
    }
    
    /**
     * 
     * Fetches all tags and their counts
     * 
     * Returns an array with 'tag' => count pairs
     * 
     * @param int $node_id Node id for which to search tags for
     * 
     * @param string $handle User handle for which to search tags for
     * 
     * @return array Tag => tag count pairs
     * 
     */
    public function fetchAllCounts($node_id = null, $handle = null)
    {
        $select = Solar::factory('Solar_Sql_Select');
        
        // table names
        $tagmap = $this->_tagmap->name;
        $tags   = $this->_tags->name;
        
        $select->from($tags, "name, count($tagmap.id) as count")
               ->leftJoin($tagmap, "$tags.id = $tagmap.tags_id")
               ->where($tagmap . '.area = ?', $this->_area);
        
        // fetch for node_id?
        if (! empty($node_id)) {
            $select->where($tagmap . '.node_id = ?', (int) $node_id);
        }
        
        // fetch for handle?
        if (! empty($handle)) {
            $select->where("$tagmap.handle = ?", $handle);
        }
        
        $select->group("$tags.name");
        
        return $select->fetch('pairs');
    }
    
    /**
     * 
     * Normalizes tag strings.
     * 
     * Converts "+" to " ", trims extra spaces, and removes duplicates,
     * but otherwise keeps them in order and space-separated.
     * 
     * Also converts arrays to a normalized tag string.
     * 
     * @param string|array $tags A space-separated string of tags, or a
     * sequential array of tags.
     * 
     * @return string A space-separated string of tags.
     * 
     */
    public function asString($tags)
    {
        // convert to array from string?
        if (! is_array($tags)) {
            
            // convert all "+" to spaces (this is for URL values)
            $tags = str_replace('+', ' ', $tags);
            
            // trim all surrounding spaces and extra spaces
            $tags = trim($tags);
            //$tags = preg_replace('/[ ]{2,}/', ' ', $tags);
            
            // if there is more than one tag after exploding
            // with a comma, we'll use comma as a separator
            // and trim all whitespace
            $tags = explode(',', $tags);
            foreach ($tags as &$tag) {
                $tag = trim($tag);
            }
        }
        
        // make sure each tag is unique (no double-entries)
        $tmp = array_unique($tags);
        
        // return as space-separated text
        return implode(', ', $tmp);
    }
    
    /**
     * 
     * Normalizes tag arrays.
     * 
     * Also converts strings to a normalized tag array.
     * 
     * @param string|array $tags A space-separated string of tags, or a
     * sequential array of tags.
     * 
     * @return array An array of tags.
     * 
     */
    public function asArray($tags)
    {
        // normalize to string...
        $tags = $this->asString($tags);
        
        // ... and convert to array.  for some reason,
        // if we explode on an empty tag string, we get
        // one array element of an empty string.  that
        // screws things up, so check if there are actually
        // tags in the string.
        if ($tags) {
            return explode(', ', $tags);
        } else {
            return array();
        }
    }
    
    /**
     * 
     * "Refreshes" the tags for a node_id/user by diff.
     * 
     * @param int $node_id The node_id to work with.
     * 
     * @param string $handle User handle
     * 
     * @param string|array $tags A space-separated string of tags, or a
     * sequential array of tags.  These are the replacement tags.
     * 
     * @return array New set of tags
     * 
     */
    public function refresh($tags, $node_id, $handle = null)
    {
        // get the old set of tags
        $old = $this->fetchAll($node_id, $handle);
        
        // normalize the new tags to an array
        $new = $this->asArray($tags);
        
        // diff the tagsets
        $diff = $this->_diff($old, $new);
        
        // delete?
        if (! empty($diff['del'])) {
            
            $ids = array();
            foreach ($diff['del'] as $tag) {
                // fetch tag's id
                // @todo this sucks, make a better query
                $ids[] = (int) $this->_tags->fetchId($tag);
            }
            
            $where = array(
                'area = ?'       => $this->_area,
                'handle = ?'     => $handle,
                'node_id = ?'    => $node_id,
                'tags_id IN (?)' => $ids,
            );
            
            $this->_tagmap->delete($where);
        };
        
        // insert
        foreach ($diff['ins'] as $tag) {
            // check if tag exists
            $id = $this->_tags->fetchId($tag);
            if ($id === false) {
                // add tag
                $data = $this->_tags->insert(array('name' => $tag));
                
                // get the id
                $id = $data['id'];
            }
            
            // tag data to the tag map
            $data = array(
                'area'         => $this->_area,
                'handle' => $handle,
                'node_id'      => $node_id,
                'tags_id'      => $id,
            );
            
            // insert tag data
            $this->_tagmap->insert($data);
        }
        
        // done! return new set of tags
        return $new;
    }
    
    /**
     * 
     * Determines the diff (delete/insert matrix) between two tag sets.
     * 
     * <code>
     * $old = array('a', 'b', 'c');
     * $new = array('c', 'd', 'e');
     * 
     * // perform the diff
     * $diff = $this->_diff($old, $new);
     * 
     * // the results are:
     * // $diff['del'] == array('a', 'b');
     * // $diff['ins'] == array('d', 'e');
     * // 'c' doesn't show up because it's present in both sets
     * </code>
     * 
     * @param array $old The old (previous) set of tags.
     * 
     * @param array $new The new (current) set of tags.
     * 
     * @return array An associative array of two keys: 'del' (where the
     * value is a sequential array of tags removed from the old set)
     * and 'ins' (where the value is a sequential array of tags to be
     * added from the new set).
     * 
     */
    protected function _diff($old, $new)
    {
        // find intersections first
        $intersect = array_intersect($old, $new);
        
        // now flip arrays so we can unset easily by key
        $old = array_flip($old);
        $new = array_flip($new);
        
        // remove intersections from each array
        foreach ($intersect as $val) {
            unset($old[$val]);
            unset($new[$val]);
        }
        
        // keys remaining in $old are to be deleted,
        // keys remaining in $new are to be added
        return array(
            'del' => (array) array_keys($old),
            'ins' => (array) array_keys($new)
        );
    }
}

